HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/kevin-demo/wp-content/plugins/allspice/includes/membership-blocks.php
<?php
// includes/membership-blocks.php
//
// Editable membership sales page, in two pieces:
//
//   1. Block pattern `allspice/membership-sales-page` - plain core Gutenberg blocks
//      (headings, paragraphs, columns, quote) the publisher edits like any other page
//      content, with the dynamic CTA block dropped in at three points.
//
//   2. Server-rendered block `allspice/membership-cta` - the ONLY dynamic piece. It renders
//      current synced pricing plus the three standard membership actions (join / login /
//      manage) as the same #allspice-* hash + data-allspice-action hooks every other
//      membership surface uses. Checkout/account management stays in the widget; the page
//      bundle's landing-state applier re-flips visibility client-side on auth events
//      (matching the anonymous/active initial state PHP renders from the member cookie).
//
// Style ownership: visual styling comes from core blocks + the active theme + publisher
// customization. Only minimal scoped CSS ships for the CTA block (button row layout and an
// outline-secondary look riding `wp-element-button`, which themes style).

if (!defined('ABSPATH')) exit;

const ALLSPICE_MEMBERSHIP_PATTERN_NAME = 'allspice/membership-sales-page';
const ALLSPICE_MEMBERSHIP_CTA_BLOCK = 'allspice/membership-cta';

/* --------------------------------------------------------------------------- pricing */

/*
 * LIVE pricing from the Public API's unauthenticated offer endpoint
 * (GET /v1/memberships/{siteMembershipId}) - the synced schema-v2 payload is NOT
 * a pricing catalog (memberships.products carries included benefits + custom
 * benefits only; the old cents-field readers were dead code and are removed).
 *
 * Server-to-server, no credentials of any kind (public endpoint), short timeout,
 * and successful safe responses cached in a transient for ~5 minutes keyed by
 * siteMembershipId. Failures return null and the CTA silently omits pricing,
 * page rendering and the Join/Login/Manage actions never depend on this call,
 * and gate enforcement never touches it.
 */
const ALLSPICE_MEMBERSHIP_OFFER_TTL = 300;

function allspice_membership_offer_transient_key(string $site_membership_id): string {
    return 'allspice_mp_offer_' . md5($site_membership_id);
}

function allspice_membership_offer_fetch(string $site_membership_id) {
    $site_membership_id = trim($site_membership_id);
    if ($site_membership_id === '' || !function_exists('allspice_public_api_base')) return null;
    $key = allspice_membership_offer_transient_key($site_membership_id);
    $cached = get_transient($key);
    if (is_array($cached)) return $cached;
    $url = allspice_public_api_base() . '/memberships/' . rawurlencode($site_membership_id);
    /* PUBLIC endpoint: Accept only. Never the webhook token, reader tokens,
       nonces, or any personal data. */
    $resp = wp_remote_get($url, ['timeout' => 5, 'headers' => ['Accept' => 'application/json']]);
    if (is_wp_error($resp)) return null;
    if ((int)wp_remote_retrieve_response_code($resp) !== 200) return null;
    $json = json_decode((string)wp_remote_retrieve_body($resp), true);
    if (!is_array($json)) return null;
    /* Store only the safe display subset. */
    $prices = [];
    foreach ((array)($json['prices'] ?? []) as $price) {
        if (!is_array($price)) continue;
        $amount = $price['unitAmount'] ?? null;
        $recurring = isset($price['recurring']) && is_array($price['recurring']) ? $price['recurring'] : null;
        if (!is_numeric($amount) || (int)$amount <= 0 || $recurring === null) continue;
        $prices[] = [
            'unit_amount' => (int)$amount,
            'currency' => strtolower((string)($price['currency'] ?? 'usd')),
            'interval' => (string)($recurring['interval'] ?? ''),
            'interval_count' => max(1, (int)($recurring['intervalCount'] ?? 1)),
        ];
    }
    $offer = [
        'prices' => $prices,
        'trial_days' => max(0, (int)($json['trialDays'] ?? 0)),
        'signups_enabled' => ($json['signupsEnabled'] ?? true) !== false,
    ];
    set_transient($key, $offer, ALLSPICE_MEMBERSHIP_OFFER_TTL); /* successes only */
    return $offer;
}

/* Offer -> the pricing struct the line builder consumes. Monthly/annual are the
   plain 1-count intervals; anything more exotic is ignored for the summary line. */
function allspice_membership_pricing_from_offer($offer): array {
    $monthly = null;
    $annual = null;
    if (is_array($offer)) {
        foreach ((array)($offer['prices'] ?? []) as $price) {
            if (!is_array($price) || (int)($price['interval_count'] ?? 1) !== 1) continue;
            if ($price['interval'] === 'month' && $monthly === null) $monthly = (int)$price['unit_amount'];
            if ($price['interval'] === 'year' && $annual === null) $annual = (int)$price['unit_amount'];
        }
    }
    $discount = null;
    if ($monthly !== null && $annual !== null) {
        $monthly_total = $monthly * 12;
        if ($monthly_total > 0 && $annual < $monthly_total) {
            $pct = (int)round((($monthly_total - $annual) / $monthly_total) * 100);
            if ($pct > 0) $discount = $pct;
        }
    }
    return [
        'monthly_cents' => $monthly,
        'annual_cents' => $annual,
        'discount_pct' => $discount,
        'trial_days' => is_array($offer) ? (int)($offer['trial_days'] ?? 0) : 0,
    ];
}

function allspice_membership_format_price_cents(int $cents): string {
    return '$' . number_format($cents / 100, 2);
}

/* "$5.00/mo or $48.00/year (20% off)" | monthly-only | annual-only | '' when no prices. */
function allspice_membership_cta_pricing_line(array $pricing): string {
    $m = $pricing['monthly_cents'] ?? null;
    $a = $pricing['annual_cents'] ?? null;
    $d = $pricing['discount_pct'] ?? null;
    if ($m === null && $a === null) return '';
    if ($m !== null && $a === null) return allspice_membership_format_price_cents($m) . '/mo';
    if ($m === null && $a !== null) return allspice_membership_format_price_cents($a) . '/year';
    $line = allspice_membership_format_price_cents($m) . '/mo or '
        . allspice_membership_format_price_cents($a) . '/year';
    if ($d !== null && $d > 0) $line .= ' (' . $d . '% off)';
    return $line;
}

/* ------------------------------------------------------------------------- CTA block */

/*
 * Pure markup builder (unit-testable without WP). $active is the verified member-session
 * server state; hidden buttons stay in the DOM with display:none so the page bundle's
 * landing-state applier (which flips el.style.display by data-allspice-action) can toggle
 * them on login/logout/checkout events without re-rendering.
 */
function allspice_membership_cta_markup(array $pricing, bool $active, string $wrapper_attrs, bool $signups_open = true, string $benefits_html = ''): string {
    $pricing_line = allspice_membership_cta_pricing_line($pricing);
    $anon = $active ? ' style="display:none"' : '';
    $member = $active ? '' : ' style="display:none"';
    /* Configured labels from the ONE normalizer; hardcoded strings only as fallback. */
    $labels = function_exists('allspice_membership_gate_copy')
        ? allspice_membership_gate_copy('content_immediate')
        : ['join_label' => 'Become a member', 'login_label' => 'Already a member? Log in'];

    $html = (function_exists('allspice_gate_embedded_css') ? allspice_gate_embedded_css() : '')
        . '<div ' . $wrapper_attrs . '>' . $benefits_html;
    if ($pricing_line !== '' && $signups_open) {
        $html .= '<p class="allspice-membership-cta__pricing">' . esc_html($pricing_line) . '</p>';
        $trial_days = (int)($pricing['trial_days'] ?? 0);
        if ($trial_days > 0) {
            $html .= '<p class="allspice-membership-cta__trial">'
                . esc_html(sprintf('%d-day free trial', $trial_days)) . '</p>';
        }
    }
    if (!$signups_open && !$active) {
        /* Closed signups (schema v2 new_signups_enabled === false): NO active Join button,
           existing members are untouched (Manage below still works and full access stays). */
        $html .= '<p class="allspice-membership-cta__closed">'
            . esc_html(function_exists('allspice_membership_closed_signups_message')
                ? allspice_membership_closed_signups_message()
                : 'Membership is not currently accepting new signups.')
            . '</p>';
    }
    $html .= '<div class="allspice-membership-cta__buttons">';
    if ($signups_open) {
        $html .= '<a class="wp-block-button__link wp-element-button allspice-membership-cta__join"'
            . ' href="#allspice-membership" data-allspice-action="open_membership" role="button"' . $anon . '>'
            . esc_html($labels['join_label']) . '</a>';
    }
    $html .= '<a class="wp-block-button__link wp-element-button allspice-membership-cta__login allspice-membership-cta__btn--secondary"'
        . ' href="#allspice-login" data-allspice-action="open_login" role="button"' . $anon . '>'
        . esc_html($labels['login_label']) . '</a>'
        . '<a class="wp-block-button__link wp-element-button allspice-membership-cta__manage"'
        . ' href="#allspice-account" data-allspice-action="open_account" role="button"' . $member . '>'
        . esc_html__('Manage membership', 'allspice') . '</a>'
        . '</div>';
    return $html . '</div>';
}

/* Render callback: synced pricing + verified member cookie -> markup. Block supports
   (align/color/spacing/typography/className) arrive via get_block_wrapper_attributes. */
function allspice_membership_cta_render($attributes = []): string {
    $attributes = is_array($attributes) ? $attributes : [];
    $classes = 'allspice-membership-cta';
    $text_align = trim((string)($attributes['textAlign'] ?? 'center'));
    if (in_array($text_align, ['left', 'center', 'right'], true)) {
        $classes .= ' has-text-align-' . $text_align;
    }
    $wrapper_attrs = function_exists('get_block_wrapper_attributes')
        ? get_block_wrapper_attributes(['class' => $classes])
        : 'class="' . esc_attr($classes) . '"';
    $active = function_exists('allspice_member_session_summary')
        ? !empty(allspice_member_session_summary()['active'])
        : false;
    $signups_open = function_exists('allspice_memberships_new_signups_enabled')
        ? allspice_memberships_new_signups_enabled() : true;
    /* showBenefits: repeated CTAs on one page should not all repeat the list
       (generated page: first true, later ones false). Default true. */
    $show_benefits = ($attributes['showBenefits'] ?? true) !== false;
    $benefits = $show_benefits && function_exists('allspice_membership_benefits_html')
        ? allspice_membership_benefits_html(0) : '';
    /* LIVE pricing from the public offer endpoint (transient-cached). The synced
       config supplies only the id; enforcement/access never touch this call, and
       a failed fetch just renders the CTA without a pricing line. */
    $pricing = ['monthly_cents' => null, 'annual_cents' => null, 'discount_pct' => null, 'trial_days' => 0];
    $n = function_exists('allspice_memberships_normalized') ? allspice_memberships_normalized() : null;
    if ($n !== null && $n['site_membership_id'] !== '' && $signups_open) {
        $pricing = allspice_membership_pricing_from_offer(
            allspice_membership_offer_fetch($n['site_membership_id'])
        );
    }
    return allspice_membership_cta_markup($pricing, $active, $wrapper_attrs, $signups_open, $benefits);
}

add_action('init', 'allspice_membership_register_cta_block');
function allspice_membership_register_cta_block(): void {
    if (!function_exists('register_block_type')) return;

    /* No-build editor script: ServerSideRender preview + alignment toolbar. */
    wp_register_script(
        'allspice-membership-cta-editor',
        false,
        ['wp-blocks', 'wp-element', 'wp-block-editor', 'wp-server-side-render', 'wp-components'],
        defined('ALLSPICE_PLUGIN_VERSION') ? ALLSPICE_PLUGIN_VERSION : false,
        true
    );
    wp_add_inline_script('allspice-membership-cta-editor', allspice_membership_cta_editor_js());

    register_block_type(ALLSPICE_MEMBERSHIP_CTA_BLOCK, [
        'api_version' => 2,
        'title' => __('Allspice Membership CTA', 'allspice'),
        'description' => __('Current membership pricing with Join / Log in / Manage actions. Checkout runs in the Allspice widget.', 'allspice'),
        'category' => 'widgets',
        'attributes' => [
            'textAlign' => ['type' => 'string', 'default' => 'center'],
            'showBenefits' => ['type' => 'boolean', 'default' => true],
        ],
        'supports' => [
            'align' => ['wide', 'full'],
            'color' => ['background' => true, 'text' => true, 'link' => true],
            'spacing' => ['margin' => true, 'padding' => true],
            'typography' => ['fontSize' => true],
            'customClassName' => true,
            'html' => false,
        ],
        'render_callback' => 'allspice_membership_cta_render',
        'editor_script' => 'allspice-membership-cta-editor',
    ]);

    if (function_exists('register_block_style')) {
        /* "Outline" block style: publishers flip the whole CTA to outline buttons. */
        register_block_style(ALLSPICE_MEMBERSHIP_CTA_BLOCK, [
            'name' => 'outline',
            'label' => __('Outline buttons', 'allspice'),
        ]);
    }
}

function allspice_membership_cta_editor_js(): string {
    return <<<'JS'
( function ( wp ) {
    if ( ! wp || ! wp.blocks || ! wp.element ) return;
    var el = wp.element.createElement;
    var be = wp.blockEditor || wp.editor;
    var ServerSideRender = wp.serverSideRender;
    wp.blocks.registerBlockType( 'allspice/membership-cta', {
        title: 'Allspice Membership CTA',
        category: 'widgets',
        icon: 'money-alt',
        attributes: {
            textAlign: { type: 'string', default: 'center' },
            showBenefits: { type: 'boolean', default: true }
        },
        supports: {
            align: [ 'wide', 'full' ],
            color: { background: true, text: true, link: true },
            spacing: { margin: true, padding: true },
            typography: { fontSize: true },
            customClassName: true,
            html: false
        },
        edit: function ( props ) {
            var blockProps = be && be.useBlockProps ? be.useBlockProps() : {};
            var inspector = null;
            if ( be && be.InspectorControls && wp.components && wp.components.PanelBody && wp.components.ToggleControl ) {
                inspector = el( be.InspectorControls, { key: 'inspector' },
                    el( wp.components.PanelBody, { title: 'Membership CTA' },
                        el( wp.components.ToggleControl, {
                            label: 'Show benefits list',
                            checked: props.attributes.showBenefits !== false,
                            onChange: function ( v ) { props.setAttributes( { showBenefits: !!v } ); }
                        } ) ) );
            }
            var controls = null;
            if ( be && be.BlockControls && be.AlignmentToolbar ) {
                controls = el( be.BlockControls, { key: 'controls' },
                    el( be.AlignmentToolbar, {
                        value: props.attributes.textAlign,
                        onChange: function ( v ) { props.setAttributes( { textAlign: v || 'center' } ); }
                    } ) );
            }
            var preview = ServerSideRender
                ? el( ServerSideRender, { key: 'preview', block: 'allspice/membership-cta', attributes: props.attributes } )
                : el( 'p', { key: 'preview' }, 'Allspice Membership CTA' );
            return el( 'div', blockProps, inspector, controls, preview );
        },
        save: function () { return null; }
    } );
} )( window.wp );
JS;
}

/* Minimal scoped CSS - layout only; colors/typography stay with the theme. */
add_action('wp_enqueue_scripts', 'allspice_membership_cta_styles', 23);
function allspice_membership_cta_styles(): void {
    wp_add_inline_style('allspice-gate', allspice_membership_cta_css());
}
function allspice_membership_cta_css(): string {
    return '.allspice-membership-cta__buttons{display:flex;flex-wrap:wrap;gap:10px;justify-content:center}'
        . '.allspice-membership-cta.has-text-align-left .allspice-membership-cta__buttons{justify-content:flex-start}'
        . '.allspice-membership-cta.has-text-align-right .allspice-membership-cta__buttons{justify-content:flex-end}'
        . '.allspice-membership-cta__buttons a{text-decoration:none;cursor:pointer}'
        . '.allspice-membership-cta__pricing{margin:0 0 4px;font-size:.9em;opacity:.85}'
        . '.allspice-membership-cta__trial{margin:0 0 10px;font-size:.85em;font-weight:600}'
        . '.allspice-membership-cta__btn--secondary,'
        . '.allspice-membership-cta.is-style-outline .wp-block-button__link'
        . '{background:transparent;color:inherit;border:1px solid currentColor}';
}

/*
 * Editor canvas styles: the page editor loads NONE of the front-end handles, so the
 * generated sales page rendered unstyled while being edited - glued CTA buttons,
 * unaligned benefit checks (seen on baking4happiness 2026-08-25). enqueue_block_assets
 * reaches the editor's iframed canvas (the supported path since WP 6.3); the is_admin()
 * guard keeps the front end on the single allspice-gate handle with no duplicate.
 */
add_action('enqueue_block_assets', 'allspice_membership_editor_styles');
function allspice_membership_editor_styles(): void {
    if (!is_admin()) return;
    wp_register_style('allspice-gate-editor', false, [], defined('ALLSPICE_PLUGIN_VERSION') ? ALLSPICE_PLUGIN_VERSION : null);
    wp_enqueue_style('allspice-gate-editor');
    wp_add_inline_style('allspice-gate-editor',
        (function_exists('allspice_gate_css') ? allspice_gate_css() : '') . allspice_membership_cta_css());
}

/* --------------------------------------------------- sales page content + block pattern */
/*
 * Sales-page content is normal editable core blocks; the three CTA placements are the dynamic
 * block above. TWO entry points share one skeleton:
 *   allspice_membership_sales_page_content()          program-aware - Create / Regenerate
 *   allspice_membership_sales_page_pattern_content()  neutral      - global block pattern
 * A globally registered pattern cannot know (or stay in step with) one site's membership
 * config; a page generated FOR this site can, and must.
 */

/*
 * Fallback description for an ENABLED built-in benefit the publisher left blank. Specific to
 * the benefit, never generic filler - the "Member benefits" columns exist to say
 * what that one benefit actually is. Unknown/custom ids return '' and their column simply
 * carries no paragraph rather than an invented claim about someone else's benefit.
 */
function allspice_membership_benefit_fallback_description(string $id): string {
    $map = [
        'mealPlans' => 'Get every member meal plan, ready to cook from.',
        'recipes' => 'Unlock the full members-only recipe library.',
        'articles' => 'Read member-exclusive articles and guides.',
        'pages' => 'Reach the member-only resources on this site.',
        'premiumPico' => 'Use the premium recipe helper on every recipe.',
        'adFree' => 'Read every post and recipe without ads.',
    ];
    return $map[$id] ?? '';
}

/*
 * "Member benefits" columns built from a normalized benefits list. Rows of at most three columns
 * so a program with five benefits still lays out sensibly. Titles/descriptions come from the
 * synced config and are escaped.
 *
 * An empty list yields a placeholder column, not an empty columns block: a publisher who
 * generates the page before configuring benefits gets something to edit rather than a hole.
 */
function allspice_membership_benefit_columns(array $benefits): string {
    $cells = [];
    foreach ($benefits as $b) {
        if (!is_array($b)) continue;
        $title = trim((string)($b['title'] ?? ''));
        if ($title === '') continue;
        $desc = trim((string)($b['description'] ?? ''));
        if ($desc === '') $desc = allspice_membership_benefit_fallback_description((string)($b['id'] ?? ''));
        $cell = '<!-- wp:column -->' . "\n"
            . '<div class="wp-block-column"><!-- wp:heading {"textAlign":"center","level":3} -->' . "\n"
            . '<h3 class="wp-block-heading has-text-align-center">' . esc_html($title) . '</h3>' . "\n"
            . '<!-- /wp:heading -->';
        if ($desc !== '') {
            $cell .= "\n\n" . '<!-- wp:paragraph {"align":"center"} -->' . "\n"
                . '<p class="has-text-align-center">' . esc_html($desc) . '</p>' . "\n"
                . '<!-- /wp:paragraph -->';
        }
        $cell .= '</div>' . "\n" . '<!-- /wp:column -->';
        $cells[] = $cell;
    }

    if ($cells === []) {
        $cells[] = '<!-- wp:column -->' . "\n"
            . '<div class="wp-block-column"><!-- wp:heading {"textAlign":"center","level":3} -->' . "\n"
            . '<h3 class="wp-block-heading has-text-align-center">[Add a member benefit]</h3>' . "\n"
            . '<!-- /wp:heading -->' . "\n\n"
            . '<!-- wp:paragraph {"align":"center"} -->' . "\n"
            . '<p class="has-text-align-center">[Describe what members get. Enabled membership '
            . 'benefits are filled in here automatically when the page is generated.]</p>' . "\n"
            . '<!-- /wp:paragraph --></div>' . "\n"
            . '<!-- /wp:column -->';
    }

    $rows = [];
    foreach (array_chunk($cells, 3) as $chunk) {
        $rows[] = '<!-- wp:columns -->' . "\n"
            . '<div class="wp-block-columns">' . implode("\n\n", $chunk) . '</div>' . "\n"
            . '<!-- /wp:columns -->';
    }
    return implode("\n\n", $rows);
}

/*
 * Hero heading: the site's own name where WordPress can supply one ("Become a
 * Simply Tasty member"), a neutral fallback where it cannot. Resolved at generation /
 * pattern-registration time and then ordinary editable content, like everything else
 * in the skeleton.
 */
function allspice_membership_sales_page_heading(): string {
    $site = function_exists('get_bloginfo') ? trim((string)get_bloginfo('name')) : '';
    return $site !== '' ? sprintf('Become a %s member', $site) : 'Become a Member';
}

/*
 * The page skeleton. Everything specific to one program arrives as parameters, so the
 * program-aware generator and the neutral global pattern share one layout and can never
 * drift apart structurally while differing in what they claim.
 *
 * No testimonial section: shipping placeholder quotes invites publishers to leave
 * fabricated social proof live. Publishers who have real member quotes can add a quote
 * block anywhere - this is ordinary editable page content.
 */
function allspice_membership_sales_page_skeleton(string $hero_sentence, string $benefit_columns, string $faq_sentence): string {
    /* Only the hero CTA repeats the benefits list; the mid-page and closing CTAs
       stay compact (the page's own benefit columns already made the case). */
    $cta_first = '<!-- wp:allspice/membership-cta {"showBenefits":true} /-->';
    $cta = '<!-- wp:allspice/membership-cta {"showBenefits":false} /-->';
    $heading = esc_html(allspice_membership_sales_page_heading());
    $hero = esc_html($hero_sentence);
    $faq = esc_html($faq_sentence);

    return <<<HTML
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull"><!-- wp:heading {"textAlign":"center","level":1} -->
<h1 class="wp-block-heading has-text-align-center">{$heading}</h1>
<!-- /wp:heading -->

<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">{$hero}</p>
<!-- /wp:paragraph -->

{$cta_first}</div>
<!-- /wp:group -->

<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">Member benefits</h2>
<!-- /wp:heading -->

{$benefit_columns}

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">{$cta}</div>
<!-- /wp:group -->

<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">Frequently Asked Questions</h2>
<!-- /wp:heading -->

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">What happens after I join?</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>{$faq}</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">How do I manage or cancel my membership?</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Use the Manage membership button on this page to update your payment details or cancel. Canceling stops future charges, and everything you've already paid for stays available through the end of that billing period.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">How do I log in?</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Click Log in on this page (or in the recipe helper) and use the email address you joined with - no separate password for this website is needed.</p>
<!-- /wp:paragraph -->

<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull">{$cta}</div>
<!-- /wp:group -->
HTML;
}

/* Neutral copy shared by both entry points. It promises the membership, never a particular
   benefit - the "Member benefits" columns are the only place a specific claim is made, and
   they are built from what the publisher actually enabled. */
const ALLSPICE_MEMBERSHIP_HERO_SENTENCE = 'Support the site and unlock everything included with membership.';
const ALLSPICE_MEMBERSHIP_FAQ_SENTENCE = "You'll check out with your email address and be signed in here the moment payment completes. Everything your membership includes is available from that point on.";

/*
 * PROGRAM-AWARE generated page content: the "Member benefits" columns come from the ENABLED
 * benefits in the live normalized model. Used by Create / Regenerate Membership Page.
 *
 * The hardcoded columns this replaced advertised ad-free browsing, members-only recipes and
 * exclusive articles unconditionally - a publisher who had disabled any of them shipped a
 * page promising something their membership does not include.
 *
 * EDITABILITY CONTRACT: this resolution happens ONCE, at generation time. The result is
 * ordinary publisher-editable post content from that moment on; nothing here ever rewrites an
 * existing page, and a later settings change does not touch it. (The dynamic CTA block is the
 * deliberate exception - it renders live benefits and pricing on every request.)
 */
function allspice_membership_sales_page_content(): string {
    $benefits = [];
    if (function_exists('allspice_memberships_normalized')) {
        $n = allspice_memberships_normalized();
        if (is_array($n) && isset($n['benefits']) && is_array($n['benefits'])) {
            $benefits = $n['benefits'];
        }
    }
    return allspice_membership_sales_page_skeleton(
        ALLSPICE_MEMBERSHIP_HERO_SENTENCE,
        allspice_membership_benefit_columns($benefits),
        ALLSPICE_MEMBERSHIP_FAQ_SENTENCE
    );
}

/*
 * Global block pattern content, kept generic. Patterns register on `init` for every
 * site and are inserted into arbitrary posts, so this cannot know (or stay in step with) one
 * site's membership config - a pattern that baked in benefits would be stale the moment the
 * publisher changed them. Placeholders make no specific promise and prompt an edit.
 */
function allspice_membership_sales_page_pattern_content(): string {
    return allspice_membership_sales_page_skeleton(
        ALLSPICE_MEMBERSHIP_HERO_SENTENCE,
        allspice_membership_benefit_columns([]),
        ALLSPICE_MEMBERSHIP_FAQ_SENTENCE
    );
}

add_action('init', 'allspice_membership_register_sales_page_pattern');
function allspice_membership_register_sales_page_pattern(): void {
    if (!function_exists('register_block_pattern')) return;
    register_block_pattern(ALLSPICE_MEMBERSHIP_PATTERN_NAME, [
        'title' => __('Allspice Membership Sales Page', 'allspice'),
        'description' => __('Editable membership sales landing page generated by the Allspice plugin.', 'allspice'),
        'categories' => ['call-to-action'],
        /* NOT the program-aware generator: see allspice_membership_sales_page_pattern_content. */
        'content' => allspice_membership_sales_page_pattern_content(),
    ]);
}